iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 11

Day-011. 一些讓你看來很強的全端 TRPC 伴讀 - TodoList (下)

  • 分享至 

  • xImage
  •  

今天我們繼續完成剩下的部分,昨天已經完成 GET / posts 的 api,也順利接值了,今天繼續完 CREATE DELETE UPDATE 的部分。

完成 api

昨天的 post 資料我們是透過 prisma studio 去添加的,但實際開發中我們並不會這麼做,主要是讓讀者可以大概知道 prisma studio 有什麼內容,所以接下來就來實現如何 create post

首先先回顧一下昨日定義的 schema

// @/validate/api/post
import { z } from "zod";

export const getPostSchema = z.object({
  post_id: z.string()
})
export type GetPostSchema = z.infer<typeof getPostSchema>

export const createPostSchema = z.object({
  title: z.string().min(1, { message: 'title required' }),
  content: z.string().optional(),
  published: z.boolean().default(false)
})
export type CreatePostSchema = z.infer<typeof createPostSchema>


export const togglePostPuPublishedSchema = z.object({
  id: z.number(),
  published: z.boolean()
})
export type TogglePostPuPublishedSchema = z.infer<typeof togglePostPuPublishedSchema>


export const deletePostSchema = z.object({
  id: z.number()
})
export type DeletePostSchema = z.infer<typeof deletePostSchema>

之後創建 addPostapi,在 trpc 中並不像 RESTful API 有各種 https methods 例如 GETPOSTDELETE 等等,取而代之的就是除了 GET 以外是透過 .query() ,其餘都是 .mutation() 涵蓋一切,用起來跟 graphql 一樣。

這邊的 addPost 邏輯很簡單:

  1. 預防重複的 post 添加到 db,會先找尋 duplicatePost 如果有救throw error。
  2. 根據 prisma.post.create 結果返回 postid
// ~src/server/api/post.ts
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import { TRPCError } from "@trpc/server";
import {
  getPostSchema,
  createPostSchema,
  togglePostPuPublishedSchema,
  deletePostSchema
} from "@/validate/api/post";

export const postsRouter = router({
//..
  addPost: publicProcedure
    .input(createPostSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx
      const duplicatePost = await prisma.post.findFirst({
        where: {
          title: input.title
        }
      })
      if (duplicatePost) {
        throw new TRPCError({ code: 'CONFLICT', message: 'Title already exists' })
      }
      const { id: postId } = await prisma.post.create({
        data: input,
        select: {
          id: true
        }
      })

      return { message: 'success create post', id: postId }

    })
})

prisma 中可以透過 select 選擇你要 return 的 fields,這樣的寫法假如你的 data 有很多 key 可以大大減少不需要 return 的資料,除了提升 query 效能外,可讀性也有幫助。

const { id: postId } = await prisma.post.create({
    data: input,
    select: {
      id: true
    }
})

之後我們到 PostForm 引入 addPostapi.posts.addPost.useMutationreturn
mutateAsyncfunction,我們在 onSubmit 把我們 form data 傳參數給他,然後在 useMutation 中還有 onSuccessonError callback function,用來驗證 mutateAsync 成功與否。

// ~src/components/PostForm.tsx
import { RouterInputs, api } from '@/utils/api'
import { createPostSchema, type CreatePostSchema } from '@/validate/api/post'
import { zodResolver } from '@hookform/resolvers/zod'
import React from 'react'
import { FieldError, FieldErrors, SubmitHandler, useForm } from 'react-hook-form'
import { Input } from './Input'
import { Button } from './Button'
import { TRPCClientError } from '@trpc/client'
import { queryClient } from './Provider'

export const PostForm = () => {
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {
      console.log('onSuccess')
    },
    onError: (e) => {
      if (e instanceof TRPCClientError) {
        console.log('TRPCClientError')
      }
    }
  })
  const { register, formState: { errors }, handleSubmit } = useForm<CreatePostSchema>({
    resolver: zodResolver(createPostSchema),
    mode: 'onChange',
    defaultValues: {
      published: false
    }
  })
  const onSubmit: SubmitHandler<CreatePostSchema> = async (data) => {
    await createPost(data)
  }

  return (
    <div
      className="
        bg-white
          px-4
          py-8
          shadow
          sm:rounded-lg
          sm:px-10
        "
    >
     // ..

    </div>
  );
}

但這時你發現我明明按下 submit ,卻沒有新增 post

此時你重新整理畫面後資料就出現了。

但顯然這不是我們要的結果,雖然說我們成功新增 post datadb ,但我們 client 端得 post lists 並沒有新的內容。

原因在於 query cache 並沒有更新。

重新整理前

重新整理後

所以問題是因為 react query cache 並沒有 update 內容,相信有用過 react query 的讀者肯定知道可以透過 query key 方式透過呼叫 queryClient.invalidateQueries 去更新 query cache 內容,但 trpc 有提供 useContext 整合 invalidate 功能。

import { queryClient } from './Provider'

export const PostForm = () => {
  const utils = api.useContext()
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey:['yourKEY']
        })
    }

透過 useContext 我們就可以根據 route 去 invalidate query cache 得內容摟~

export const PostForm = () => {
  const utils = api.useContext()
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {

      utils.posts.getPosts.invalidate()
      console.log('onSuccess')
    },

這樣我們每次 create post getPosts 內容就會重新 update 了。

這邊簡單補充 trpc invalidate 功能。

如果你希望每次執行 useMutation 自動更新所有 query cache 內容,而不是一個一個根據 router invalidate 的話你可以這樣寫。

export const trpc = createTRPCReact<AppRouter, SSRContext>({
  overrides: {
    useMutation: {
      /**
       * This function is called whenever a `.useMutation` succeeds
       **/
      async onSuccess(opts) {
        /**
         * @note that order here matters:
         * The order here allows route changes in `onSuccess` without
         * having a flash of content change whilst redirecting.
         **/
        // Calls the `onSuccess` defined in the `useQuery()`-options:
        await opts.originalFn();
        // Invalidate all queries in the react-query cache:
        await opts.queryClient.invalidateQueries();
      },
    },
  },
});

或是你只想在特定的 useMutation invalidate all query cache,可以這樣做

export const PostForm = () => {
  const utils = api.useContext()
  const { mutateAsync: createPost } = api.posts.addPost.useMutation({
    onSuccess: () => {

      utils.invalidate()
      console.log('onSuccess')
    },

剩下的 Update toggledelete post 就很簡單了,就執行 prisma.post.updateprisma.post.delete function

import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import { TRPCError } from "@trpc/server";
import {
  getPostSchema,
  createPostSchema,
  togglePostPuPublishedSchema,
  deletePostSchema
} from "@/validate/api/post";

export const postsRouter = router({
  // ..

  addPost: publicProcedure
    .input(createPostSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx
      const duplicatePost = await prisma.post.findFirst({
        where: {
          title: input.title
        }
      })
      if (duplicatePost) {
        throw new TRPCError({ code: 'CONFLICT', message: 'Title already exists' })
      }
      const { id: postId } = await prisma.post.create({
        data: input,
        select: {
          id: true
        }
      })

      return { message: 'success create post', id: postId }
    }),
  togglePostPublish: publicProcedure
    .input(togglePostPuPublishedSchema)
    .mutation(async ({ input, ctx }) => {
      const { id, published } = input
      const { prisma } = ctx
      await prisma.post.update({
        where: {
          id
        },
        data: {
          published
        }
      })
    }),
  deletePost: publicProcedure
    .input(deletePostSchema)
    .mutation(async ({ input, ctx }) => {
      const { id } = input
      const { prisma } = ctx
      await prisma.post.delete({
        where: {
          id
        }
      })

    })
})

最後再把 api 加上,這樣你就完成所有 crud 功能摟~

import { PostForm } from "@/components/PostForm";
import { api } from "@/utils/api";
import { AiFillDelete } from "react-icons/ai";

export default function Home() {
  const utils = api.useContext()
  const { data: posts, isLoading, isError, error } = api.posts.getPosts.useQuery()
  const { mutateAsync: togglePost } = api.posts.togglePostPublish.useMutation({
    onSuccess: () => {
      utils.posts.invalidate()
    }
  })
  const { mutateAsync: deletePost } = api.posts.deletePost.useMutation({
    onSuccess: () => {
      utils.posts.invalidate()
    }
  })
  if (isLoading) return 'isLoading'
  if (isError) return error.message
  return (
    <div className="bg-gray-100 min-h-screen overflow-y-auto p-4">
      <h2 className="text-center text-3xl">Create posts</h2>
      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <PostForm />
        <ul className="flex flex-col gap-[1rem] justify-center mt-5">
          {posts.map((post, index) => (
            <li key={post.id} className="flex items-center justify-between">
              <label
                htmlFor=""
                className={`
                  text-2xl 
                  ${!!post.published && "line-through"}
                `}
                onClick={async () => {
                  await togglePost({ id: post.id, published: !post.published })
                }}
              >{post.title}</label>
              <AiFillDelete
                color="red"
                className="cursor-pointer"
                size={20}
                onClick={async () => {
                  await deletePost({ id: post.id })
                }}
              />
            </li>
          ))}
        </ul>
      </div>

    </div>
  );
}

相關連結

完整的 demo 可以在這邊查看: https://github.com/Danny101201/next_demo/tree/main

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day-010. 一些讓你看來很強的全端 TRPC 伴讀 - TodoList (上)
下一篇
Day-012. 一些讓你看來很強的全端 TRPC 伴讀 - Links
系列文
一些讓你看來很強的全端- trcp 伴讀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言